package payment

import (
	"crypto"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"net/url"
	"strconv"
	"time"

	"go-share/logging"
)

var (
	ALIPAY_PUBLIC_KEY []byte
	PRIVATE_KEY       []byte
	PUBLIC_KEY        []byte
)

type AliyPay struct {
	Payment
}

const (
	ALI_PAY_URL      = "https://mapi.alipay.com/gateway.do"
	ALI_SERVCIE_NAME = "create_direct_pay_by_user"
	ALI_APP_ID       = "appid"
	ALI_PARTNER      = "partner"
	ALI_KEY          = "key"
	ALI_RETURN_URL   = "return_url"
	ALI_NOTIFY_URL   = "notify_url"
	ALI_SELLER       = "seller"
)

// 订单单数据包
type OrderJSON struct {
	ExtraParam  string  `json:"extra_common_param"`
	Service     string  `json:"service"`
	Partner     string  `json:"partner"`
	InputChar   string  `json:"_input_charset"`
	SignType    string  `json:"sign_type"`
	Sign        string  `json:"sign"`
	NotifyUrl   string  `json:"notify_url"`
	ReturnUrl   string  `json:"return_url"`
	OrderID     string  `json:"out_trade_no"`
	Subject     string  `json:"subject"` // 商品名称
	PaymentType int     `json:"payment_type"`
	TotalFee    float32 `json:"total_fee"`
	SellerID    string  `json:"seller_id"` // 卖家支付宝用户号
	Body        string  `json:"body"`      // 商品描述
	ClientIP    string  `json:"exter_invoke_ip"`
}

// 调起支付
type AlipayPullUp struct {
	AppID      string `json:"app_id"`
	Method     string `json:"method"`
	Format     string `json:"format"`
	Charset    string `json:"charset"`
	SignType   string `json:"sign_type"`
	Sign       string `json:"sign"`
	Timestamp  string `json:"timestamp"`
	Version    string `json:"version"`
	NotifyUrl  string `json:"notify_url"`
	BizContent string `json:"biz_content"`
}

type AplipyBizContent struct {
	Subject     string  `json:"subject"`
	OutTradeNO  string  `json:"out_trade_no"`
	TotalAmount float32 `json:"total_amount"`
	ProductCode string  `json:"product_code"`
}

func NewAlipay(params map[string]interface{}) *AliyPay {
	return &AliyPay{Payment{Config: params}}
}

func (this *AliyPay) GeneratePayUrl(order Order) (string, error) {
	var o OrderJSON
	o.ExtraParam = order.ExtraParam
	o.Service = ALI_SERVCIE_NAME
	o.Partner = this.getConfig(ALI_PARTNER).(string)
	o.InputChar = "utf-8"
	o.NotifyUrl = this.getConfig(ALI_NOTIFY_URL).(string)
	o.ReturnUrl = this.getConfig(ALI_RETURN_URL).(string)
	o.Subject = order.ProudctName
	o.Body = order.ProudctDescription
	o.OrderID = order.OrderID
	o.ClientIP = order.IP
	o.PaymentType = 1

	if sellerId := this.getConfig(ALI_SELLER); sellerId == nil {
		o.SellerID = this.getConfig(ALI_PARTNER).(string)
	} else {
		o.SellerID = sellerId.(string)
	}
	o.TotalFee = float32(order.PriceTotal) / 100
	params := this.struct2map(o)

	o.Sign = this.makeSign(params)
	o.SignType = "MD5"
	urlPrams := this.struct2map(o)
	return fmt.Sprintf("%s?%s", ALI_PAY_URL, this.makeUrl(urlPrams)), nil
}

func (this *AliyPay) PayNotify(notify []byte) (*NofiyData, error) {
	if len(notify) == 0 {
		return nil, fmt.Errorf("Notify Data empty")
	}
	var resultData NofiyData
	var notifyData map[string]interface{}
	notifyMap := make(map[string]interface{}, len(notifyData))
	json.Unmarshal(notify, &notifyData)
	var signTypeStr string

	for k, v := range notifyData {
		if k == "sign_type" {
			signTypeStr = fmt.Sprintf("%v", v)
			continue
		}
		if item, ok := v.([]interface{}); ok {
			notifyMap[k] = item[0]
		}

	}
	if notifyMap["trade_status"].(string) != "TRADE_SUCCESS" {
		resultData.ReturnData = []byte("Status Error")
		return &resultData, fmt.Errorf("Status Error")
	}

	var signType int
	switch signTypeStr {
	case `[MD5]`:
		signType = SIGN_TYPE_MD5
	case `[RSA]`:
		signType = SIGN_TYPE_RSA
	}

	if !this.checkSign(notifyMap, signType) {
		resultData.ReturnData = []byte("Sign Error")
		return &resultData, fmt.Errorf("Sign Error")
	}

	var tfee float64

	resultData.OrderID = notifyMap["out_trade_no"].(string)
	if signType == SIGN_TYPE_RSA {
		tfee, _ = strconv.ParseFloat(notifyMap["total_amount"].(string), 32)
	} else {
		tfee, _ = strconv.ParseFloat(notifyMap["total_fee"].(string), 32)
	}

	resultData.TotalFee = int(tfee * 100)
	resultData.TransactionID = notifyMap["trade_no"].(string)
	resultData.ReturnData = []byte("success")

	return &resultData, nil
}

func (this *AliyPay) GeneratePrepay(order Order) (interface{}, error) {
	bc := AplipyBizContent{
		Subject:     order.ProudctName,
		OutTradeNO:  order.OrderID,
		TotalAmount: float32(order.PriceTotal) / 100,
		ProductCode: "QUICK_MSECURITY_PAY",
	}

	bcByte, err := json.Marshal(bc)
	if err != nil {
		return nil, err
	}

	bcStr := string(bcByte)
	timestamp := time.Now()
	pullUp := AlipayPullUp{
		AppID:      this.getConfig(ALI_APP_ID).(string),
		Method:     "alipay.trade.app.pay",
		Format:     "JSON",
		Charset:    "utf-8",
		SignType:   "RSA",
		Timestamp:  timestamp.Format("2006-01-02 15:04:05"),
		Version:    "1.0",
		NotifyUrl:  this.getConfig(ALI_NOTIFY_URL).(string),
		BizContent: bcStr,
	}

	params := this.struct2map(pullUp)
	toSign := this.makeUrl(params)
	siganature, err := this.makeRsaSign(toSign)
	if err != nil {
		return nil, err
	}

	params = this.struct2map(pullUp)
	prepay := this.encodeValue(params)
	prepay = prepay + fmt.Sprintf("&sign=%v", url.QueryEscape(siganature))

	return prepay, nil
}

// --------------------------------------------------------------------------------
func init() {
	ALIPAY_PUBLIC_KEY, _ = ioutil.ReadFile("keys/ali_public_key.pem")
	PRIVATE_KEY, _ = ioutil.ReadFile("keys/private_key.pem")
	PUBLIC_KEY, _ = ioutil.ReadFile("keys/public_key.pem")
}

func (this *AliyPay) getConfig(name string) interface{} {
	if v, ok := this.Config[name]; !ok {
		return nil
	} else {
		return v
	}
}

func (this *AliyPay) checkConfig() bool {
	configFiled := []string{WECHAT_APP_ID, ALI_PARTNER, ALI_SELLER, ALI_KEY, ALI_NOTIFY_URL, ALI_RETURN_URL}
	for _, item := range configFiled {
		if v, ok := this.Payment.Config[item]; !ok || len(fmt.Sprintf("%v", v)) == 0 {
			logging.Error("Not found [%v] in the config map", item)
			return false
		}
	}
	return true
}

func (this *AliyPay) makeSign(params map[string]interface{}) string {
	signParams := this.makeUrl(params)
	return this.MD5Sigin(fmt.Sprintf("%s%s", signParams, this.getConfig(ALI_KEY)))
}

func (this *AliyPay) checkSign(params map[string]interface{}, signType int) bool {
	originSign := params["sign"]
	delete(params, "sign")
	var siganature string

	switch signType {
	case SIGN_TYPE_MD5:
		siganature = this.makeSign(params)
		if originSign == siganature {
			return true
		}
	case SIGN_TYPE_RSA:
		verified, err := this.verifyRsaSign(params, ALIPAY_PUBLIC_KEY, originSign.(string))
		if err == nil && verified {
			return true
		}

	default:
		return false
	}

	return false
}

func (this *AliyPay) verifyRsaSign(params map[string]interface{}, key []byte, originSign string) (bool, error) {
	toSign := this.decodeValue(params)
	signData, err := base64.StdEncoding.DecodeString(originSign)
	if err != nil {
		return false, err
	}

	block, _ := pem.Decode(key)
	if block == nil {
		return false, fmt.Errorf("Decode Public Key error")
	}

	public, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return false, err
	}

	publickKey := public.(*rsa.PublicKey)
	h := sha1.New()
	h.Write([]byte(toSign))
	digest := h.Sum(nil)

	if err := rsa.VerifyPKCS1v15(publickKey, crypto.SHA1, digest, signData); err == nil {
		return true, nil
	}

	return false, nil
}

func (this *AliyPay) makeRsaSign(data string) (string, error) {
	block, _ := pem.Decode(PRIVATE_KEY)
	if block == nil {
		return "", fmt.Errorf("rsaSign private_key error")
	}

	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return "", fmt.Errorf("Parse private_key error")
	}

	signature, err := rsaSign(data, privateKey)
	if err != nil {
		return "", err
	}

	return signature, nil
}

/////////////////////////////////////////////////

func rsaSign(origData string, privateKey *rsa.PrivateKey) (string, error) {
	h := sha1.New()
	h.Write([]byte(origData))
	digest := h.Sum(nil)

	s, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA1, digest)
	if err != nil {
		fmt.Errorf("rsaSign SignPKCS1v15 error")
		return "", err
	}
	data := base64.StdEncoding.EncodeToString(s)
	return string(data), nil
}
